Содержание

  • 1  Материалы
  • 2  Загрузка и ознакомление с данными
  • 3  Предобработка данных
    • 3.1  Переименование столбцов
    • 3.2  Обработка пропусков
    • 3.3  Обработка дубликатов
    • 3.4  Изменениие типов данных
    • 3.5  Объединение таблиц
  • 4  Исследовательский анализ данных
    • 4.1  Временной промежуток
    • 4.2  Обзор данных
    • 4.3  Сколько в среднем событий приходится на пользователя
  • 5  Основные вопросы исследования
    • 5.1  Какие есть события и как часто они встречаются
    • 5.2  Сколько пользователей совершали события
    • 5.3  Сессии
    • 5.4  Популярные сценарии совершения событий
    • 5.5  Воронки
    • 5.6  Длительности сессий
    • 5.7  Относительная частота событий
    • 5.8  Анализ источников
  • 6  Проверка гипотез
    • 6.1  Одни пользователи совершают действия tips_show и tips_click , другие — только tips_show . Проверьте гипотезу: конверсия в просмотры контактов различается у этих двух групп:
    • 6.2  Различается ли конверсия в просмотры контактов у группы пользователей пришедших из Яндекса и у группы пользователей пришедших из Google:
  • 7  Выводы

Анализ поведения пользователей в мобильном приложении¶

Анализируем данные приложения "Ненужные вещи", в котором пользователи продают и покупают вещи по принципу доски объявлений.
Наша цель - Получить на основе поведения пользователей гипотезы о том как можно было бы улучшить приложение с точки зрения пользовательского опыта, чтобы владельцы приложения могли иметь возможность управлять вовлеченностью пользователей.

Для достижения поставленной цели решить следующие задачи:

  1. Проанализировать связь целевого события — просмотра контактов — и других действий пользователей,
  2. Оценить, какие действия чаще совершают те пользователи, которые просматривают контакты.

Для достижения нашей задачи следует провести следующие действия:

  • Ознакомиться с данными,
  • Провести предобработку данных,
  • Провести исследовательский анализ данных,
  • Провести событийную аналитику,
  • Провести анализ гипотез.

Приступим к анализу данных.

Материалы¶

По проведенному исследованию представлены следующие визуализации:

  • Презентация для устного выступления

Презентация

  • Интерактивный дашборд

Дашборд

Подготовка¶

К содержанию

Для начала установим и обновим все необходимые библиотеки

In [1]:
pip install matplotlib --upgrade
Requirement already satisfied: matplotlib in c:\users\kuzne\anaconda3\lib\site-packages (3.8.2)
Requirement already satisfied: fonttools>=4.22.0 in c:\users\kuzne\anaconda3\lib\site-packages (from matplotlib) (4.25.0)
Requirement already satisfied: pillow>=8 in c:\users\kuzne\anaconda3\lib\site-packages (from matplotlib) (9.4.0)
Requirement already satisfied: contourpy>=1.0.1 in c:\users\kuzne\anaconda3\lib\site-packages (from matplotlib) (1.0.5)
Requirement already satisfied: numpy<2,>=1.21 in c:\users\kuzne\anaconda3\lib\site-packages (from matplotlib) (1.23.5)
Requirement already satisfied: cycler>=0.10 in c:\users\kuzne\anaconda3\lib\site-packages (from matplotlib) (0.11.0)
Requirement already satisfied: kiwisolver>=1.3.1 in c:\users\kuzne\anaconda3\lib\site-packages (from matplotlib) (1.4.4)
Requirement already satisfied: pyparsing>=2.3.1 in c:\users\kuzne\anaconda3\lib\site-packages (from matplotlib) (3.0.9)
Requirement already satisfied: python-dateutil>=2.7 in c:\users\kuzne\anaconda3\lib\site-packages (from matplotlib) (2.8.2)
Requirement already satisfied: packaging>=20.0 in c:\users\kuzne\anaconda3\lib\site-packages (from matplotlib) (22.0)
Requirement already satisfied: six>=1.5 in c:\users\kuzne\anaconda3\lib\site-packages (from python-dateutil>=2.7->matplotlib) (1.16.0)
Note: you may need to restart the kernel to use updated packages.
In [2]:
import pandas as pd
import datetime as dt
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import seaborn as sns
from scipy import stats as st
import numpy as np
import math as mth
import warnings
warnings.filterwarnings("ignore")
from plotly import graph_objects as go
from plotly.subplots import make_subplots

Приведем все графики к единому стилю

In [3]:
sns.set_style(style='ticks')

Загрузка и ознакомление с данными¶

К содержанию

У нас есть две таблицы:

  • mobile sources - которая содержит информацию о пользователях,
  • mobile dataset - содержит информацию о действиях в приложении.

Рассмотрим их подробнее

In [4]:
mobile_sources = pd.read_csv('mobile_sources.csv')
mobile_dataset = pd.read_csv('mobile_dataset.csv')
    
#Зададим ограничения на вывод колонок и количество символов - в данном случае "смягчим"
pd.set_option('display.max_columns', None)
pd.options.display.max_colwidth = 100

display(mobile_sources, mobile_dataset)
userId source
0 020292ab-89bc-4156-9acf-68bc2783f894 other
1 cf7eda61-9349-469f-ac27-e5b6f5ec475c yandex
2 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 yandex
3 d9b06b47-0f36-419b-bbb0-3533e582a6cb other
4 f32e1e2a-3027-4693-b793-b7b3ff274439 google
... ... ...
4288 b86fe56e-f2de-4f8a-b192-cd89a37ecd41 yandex
4289 424c0ae1-3ea3-4f1e-a814-6bac73e48ab1 yandex
4290 437a4cd4-9ba9-457f-8614-d142bc48fbeb yandex
4291 c10055f0-0b47-477a-869e-d391b31fdf8f yandex
4292 d157bffc-264d-4464-8220-1cc0c42f43a9 google

4293 rows × 2 columns

event.time event.name user.id
0 2019-10-07 00:00:00.431357 advert_open 020292ab-89bc-4156-9acf-68bc2783f894
1 2019-10-07 00:00:01.236320 tips_show 020292ab-89bc-4156-9acf-68bc2783f894
2 2019-10-07 00:00:02.245341 tips_show cf7eda61-9349-469f-ac27-e5b6f5ec475c
3 2019-10-07 00:00:07.039334 tips_show 020292ab-89bc-4156-9acf-68bc2783f894
4 2019-10-07 00:00:56.319813 advert_open cf7eda61-9349-469f-ac27-e5b6f5ec475c
... ... ... ...
74192 2019-11-03 23:53:29.534986 tips_show 28fccdf4-7b9e-42f5-bc73-439a265f20e9
74193 2019-11-03 23:54:00.407086 tips_show 28fccdf4-7b9e-42f5-bc73-439a265f20e9
74194 2019-11-03 23:56:57.041825 search_1 20850c8f-4135-4059-b13b-198d3ac59902
74195 2019-11-03 23:57:06.232189 tips_show 28fccdf4-7b9e-42f5-bc73-439a265f20e9
74196 2019-11-03 23:58:12.532487 tips_show 28fccdf4-7b9e-42f5-bc73-439a265f20e9

74197 rows × 3 columns

В наших датасетах есть следующие данные:

  1. В таблице mobile_sources:
    • userId — идентификатор пользователя,
    • source — источник, с которого пользователь установил приложение.
  2. В таблице mobile_dataset:
    • event.time — время совершения действия,
    • user.id — идентификатор пользователя,
    • event.name — действие пользователя.

Пользователи могли совершать следующие действия:

  • advert_open — открыл карточки объявления,
  • photos_show — просмотрел фотографий в объявлении,
  • tips_show — увидел рекомендованные объявления,
  • tips_click — кликнул по рекомендованному объявлению,
  • contacts_show и show_contacts — посмотрел номер телефона,
  • contacts_call — позвонил по номеру из объявления,
  • map — открыл карту объявлений,
  • search_1 — search_7 — разные действия, связанные с поиском по сайту,
  • favorites_add — добавил объявление в избранное.

Рассмотрим данные внимательнее.

In [5]:
print(mobile_sources.info(), mobile_dataset.info());
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4293 entries, 0 to 4292
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   userId  4293 non-null   object
 1   source  4293 non-null   object
dtypes: object(2)
memory usage: 67.2+ KB
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 74197 entries, 0 to 74196
Data columns (total 3 columns):
 #   Column      Non-Null Count  Dtype 
---  ------      --------------  ----- 
 0   event.time  74197 non-null  object
 1   event.name  74197 non-null  object
 2   user.id     74197 non-null  object
dtypes: object(3)
memory usage: 1.7+ MB
None None
  1. Итак, в таблице mobile_sources есть 4293 строки, и, скорее всего, это и есть число пользователей приложения. В обеих колонках содержатся данные типа object, что соответствует записям, которые мы увидели выше.
  2. В таблице mobile_dataset есть 74197 строк. Данные во всех столбцах также записаны как тип object. Однако, в колонке event.time содержится информация о времени.
  3. Нужно провести следующие шаги предобработки данных:
    • Привести названия столбцов к snake_case,
    • На первый взгляд пропусков в таблице нет, но все равно стоит проверить наличие пропусков,
    • Проверить наличие дубликатов,
    • Привести названия событий к единым написаниям,
    • Поменять тип данных в колонке event.time
    • Объединить таблицы

Приступим к предоработке.

Предобработка данных¶

К содержанию

Для того, чтобы провести корректный исследовательский анализ и в дальнейшем ответить на поставленные заказчиком вопросы, приведём наши данные в порядок, чтобы было удобно с ними работать.

Переименование столбцов¶

К содержанию

Первое, что мы сделаем - приведем столбцы к единому написанию, а именно в формат snake_case. Это следует сделать в обеих таблицах, которыми мы располагаем.

In [6]:
mobile_dataset.columns = mobile_dataset.columns.str.replace('.', '_')
mobile_sources.columns = ['user_id','source']
display(mobile_dataset.head(), mobile_sources.head())
event_time event_name user_id
0 2019-10-07 00:00:00.431357 advert_open 020292ab-89bc-4156-9acf-68bc2783f894
1 2019-10-07 00:00:01.236320 tips_show 020292ab-89bc-4156-9acf-68bc2783f894
2 2019-10-07 00:00:02.245341 tips_show cf7eda61-9349-469f-ac27-e5b6f5ec475c
3 2019-10-07 00:00:07.039334 tips_show 020292ab-89bc-4156-9acf-68bc2783f894
4 2019-10-07 00:00:56.319813 advert_open cf7eda61-9349-469f-ac27-e5b6f5ec475c
user_id source
0 020292ab-89bc-4156-9acf-68bc2783f894 other
1 cf7eda61-9349-469f-ac27-e5b6f5ec475c yandex
2 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 yandex
3 d9b06b47-0f36-419b-bbb0-3533e582a6cb other
4 f32e1e2a-3027-4693-b793-b7b3ff274439 google

Обработка пропусков¶

К содержанию

Теперь проведем проверку наличия пропусков и решим, как мы можем их обработать, если потребуется.

In [7]:
display('mobile_dataset', pd.DataFrame(mobile_dataset.isna().sum()), 'mobile_sources',
     pd.DataFrame(mobile_sources.isna().sum()))
'mobile_dataset'
0
event_time 0
event_name 0
user_id 0
'mobile_sources'
0
user_id 0
source 0

Первое предположение о том, что у нас нет пропусков оказалось верным. Пропусков нет и это замечательно, продолжим предобработку.

Обработка дубликатов¶

К содержанию

Для начала проверим наличие явных дубликатов в таблицах.

In [8]:
print('Количество полных явных дубликатов в таблице mobile_dataset:', 
      mobile_dataset.duplicated().sum(),
      '\nКоличество полных явных дубликатов в таблице mobile_sources:',
      mobile_sources.duplicated().sum())
Количество полных явных дубликатов в таблице mobile_dataset: 0 
Количество полных явных дубликатов в таблице mobile_sources: 0

Явных полных дубликатов в обеих таблицах нет, проверим, есть ли неполные явные дубликаты в таблице mobile_sources, вдруг у нас есть пользователь, которому соответствует два источника.

In [9]:
mobile_sources.duplicated(subset='user_id').sum()
Out[9]:
0

Нет, все пользователи встречаются лишь один раз, а значит каждому пользователю соответствует только один источник.

Теперь нам стоит привести разночтения в наименовании некоторых событий к единому виду, так как они создают некоторые неявные дубликаты, а также их сложно обрабатывать. Нас инстересуют все события с search_, так как их 7 разных, и наше целевое событие тоже имеет "дубликат". Приведем их к виду search и contacts_show соответственно. А также проверим, нет ли разночтений и опечаток среди наших источников пользователей.

In [10]:
mobile_dataset['event_name'] = mobile_dataset['event_name'].replace(regex=r'^search_\d+',
                                                                    value='search')
mobile_dataset['event_name'] = mobile_dataset['event_name'].replace('show_contacts',
                                                                    'contacts_show')
print('Уникальные события:', mobile_dataset['event_name'].unique(), 
      '\nУникальные источники:', mobile_sources['source'].unique())
Уникальные события: ['advert_open' 'tips_show' 'map' 'contacts_show' 'search' 'tips_click'
 'photos_show' 'favorites_add' 'contacts_call'] 
Уникальные источники: ['other' 'yandex' 'google']

Итак, больше разночтений и неявных дубликатов нет. Еще раз проверим, не привели ли наши действия к появлению полных явных дубликатов.

In [11]:
print('Количество полных явных дубликатов в таблице mobile_dataset:', 
      mobile_dataset.duplicated().sum())
Количество полных явных дубликатов в таблице mobile_dataset: 0

Отлично, пропусков и дубликатов нет, переходим к следующему пункту.

Изменениие типов данных¶

К содержанию

В таблице mobile_dataset есть колонка, в которой содержится дата и время, когда наши пользователи совершали действия. Стоит привести её к правильному типу данных.

In [12]:
mobile_dataset['event_time'] = pd.to_datetime(mobile_dataset['event_time'], unit='ns')
mobile_dataset.dtypes
Out[12]:
event_time    datetime64[ns]
event_name            object
user_id               object
dtype: object

Отлично, теперь можно объединить нашу таблицу в единый датасет и приступить к исследовательскому анализу.

Объединение таблиц¶

К содержанию

Для комфортной работы получим из двух наших таблиц одну. Сделаем это при помощи функции merge и будем объединять по идентификаторам пользователей.

In [13]:
dataset = mobile_dataset.merge(mobile_sources, on='user_id')
dataset.sample(5)
Out[13]:
event_time event_name user_id source
19269 2019-10-12 10:08:36.561814 tips_show b3b62f2c-e603-4490-8e7a-cc60697b6b71 other
48533 2019-11-01 13:43:32.293463 tips_show 87b9b9a1-e7c1-4834-a43e-4510f177f3f9 yandex
49010 2019-10-22 13:10:44.397941 tips_show 39157e8e-2f0f-421d-839e-0cf216bd783d google
49748 2019-10-26 14:00:53.427140 contacts_show e387d029-59eb-41b9-9be5-5548389c079c google
66803 2019-10-29 21:16:52.427649 tips_show 07e18551-bea5-45f8-99bf-69ed821e54be other

Теперь изучим подробнее, какая информация нам доступна.

Исследовательский анализ данных¶

К содержанию

Временной промежуток¶

К содержанию

Посмотрим, данными за какой временной период мы обладаем, заодно проверим, точно ли мы распологаем данными от 7 октября 2019 года.

In [15]:
print('Минимальная дата:', dataset['event_time'].min().strftime("%d-%m-%Y, %H:%M:%S"),
     '\nМаксимальная дата:', dataset['event_time'].max().strftime("%d-%m-%Y, %H:%M:%S"))
Минимальная дата: 07-10-2019, 00:00:00 
Максимальная дата: 03-11-2019, 23:58:12

У нас есть информация о действиях, которые совершали пользователи в течение 4 недель - с 7 октября по 3 ноября 2019 года. Теперь оценим, полные ли данные во все представленные дни.

In [16]:
fig, ax = plt.subplots()

dataset['event_time'].hist(bins=28, figsize=(18,5), grid=False, ax=ax,)
plt.xlabel('Дата')
ax.xaxis.set_major_locator(mdates.DayLocator(interval=1))
plt.setp(ax.xaxis.get_majorticklabels(), rotation=-45 , ha="left", rotation_mode="anchor") 
plt.ylabel('Количество событий')
plt.title('Распределение событий по датам'+'\n', color='SteelBlue', fontsize=20);
plt.show()

Да, за каждый день у нас есть данные. По графику можно отметить небольшие колебания активности пользователей по дням недели. Ближе к концу недели пользователи не так охотно совершают действия. Также можно увидеть подъем совершаемых действий к концу месяца. К сожалению трудно сказать, что могло повлиять на пользователей в это время.

Обзор данных¶

К содержанию

Посмотрим, общую картину данных, содержащихся в датасете.

In [17]:
print('Общее количество пользователей:', dataset['user_id'].nunique())
print('Общее число событий:', len(dataset),
     '\nЧисло уникальных событий:', dataset['event_name'].nunique())
print('Число источников пользователей:',dataset['source'].nunique())
Общее количество пользователей: 4293
Общее число событий: 74197 
Число уникальных событий: 9
Число источников пользователей: 3

Итак, у нас есть 4293 уникальных пользователя. Все эти пользователи за 4 недели исследования совершили 74197 событий. Всего пользователи совершили 9 уникальных событий. Также, пользователи пришли из 3 источников - двух крупных и многих небольших, которые объединены в одну группу "Другие".

Сколько в среднем событий приходится на пользователя¶

К содержанию

Оценим, сколько событий в среднем совершал каждый пользователь отдельно.

In [18]:
events_per_user = dataset.pivot_table(index='user_id',
                                      values='event_name',
                                      aggfunc='count')
print('В среднем на пользователя приходится {} событий'.format(
    round(events_per_user['event_name'].mean())))

display(events_per_user.describe())
fig, axes = plt.subplots(1, 2, figsize=(15, 5), sharey=False);
plt.figure(figsize=(10,8));
fig.suptitle('Количество событий на пользователя', color='SteelBlue', fontsize=20);

sns.boxplot(ax=axes[0],
            data = events_per_user,
            y='event_name', 
            notch=True, 
            showcaps=False,
            flierprops={'marker': 'x'},
            boxprops={'facecolor': 'cadetblue'},
            medianprops={'color': 'r', 'linewidth': 2}).set(ylabel='Количество событий');
sns.boxplot(ax=axes[1],
            data = events_per_user,
            y='event_name', 
            notch=True, 
            showcaps=False,
            flierprops={'marker': 'x'},
            boxprops={'facecolor': 'cadetblue'},
            medianprops={'color': 'r', 'linewidth': 2}).set(ylabel='Количество событий');
axes[1].set_ylim(0,37);

plt.show();
В среднем на пользователя приходится 17 событий
event_name
count 4293.000000
mean 17.283252
std 29.130677
min 1.000000
25% 5.000000
50% 9.000000
75% 17.000000
max 478.000000
<Figure size 1000x800 with 0 Axes>

В среднем каждый пользователь совершал от 5 до 17 событий за исследуемый промежуток времени. Медиана - 9 событий на пользователя. Однако, есть пользователи, которые совершали до 478 событий. Возможно это кто-то искал что-то конкретное, и потратил весь месяц на поиски и общение с продавцами. А может кто-то хотел полностью обставить свою квартиру с нуля, в надежде сделать это подешевле. К сожалению здесь трудно сказать, насколько это значение является выбивающимся.

Основные вопросы исследования¶

К содержанию

Проведём анализ событий приложения, которые представлены в нашем датасете.

Какие есть события и как часто они встречаются¶

К содержанию

In [19]:
ax = dataset['event_name'].value_counts(ascending=True).plot(kind='barh',
                                                             xlabel='Событие', 
                                                             figsize=(15,8))
for i in ax.containers:
    ax.bar_label(i,label_type='edge')
ax.set_xlabel('Количество событий')
ax.set_title('События, которые совершают пользователи'+'\n',
             color='SteelBlue', fontsize=20);
  • Больше всего было показов рекомендованных объявлений tips_show - чуть больше 40 тысяч, что составляет больше половины от всех совершенных действий. Ни одно другое действие не совершалось хотябы с приблизительной частотой. К сожалению, у нас нет данных о возможностях и тонкостях работы приложения, но похоже, что оно просто заваливает пользователей рекомендациями.
    • При этом событие tips_click - просмотр рекомендованного объявления находится на предпоследнем месте среди всех совершаемых действий. Возможно создателям приложения стоит обратить внимание на настройку рекомендаций.
  • Второе по популярности действие - просмотр фото photos_show, вполне оправданно, ведь фотографий у одного объявления обычно бывает несколько, и с их помощью можно лучше убедиться в том, что вещь соответствует искомым параметрам.
  • Реже всего совершалось действие contacts_call, здесь также трудно сказать в чем проблема, возможно пользователи смотрели контакты продавца, копировали и просто не совершали звонки через само приложение, а переходили в системное приложение телефона.

Сколько пользователей совершали события¶

К содержанию

С общим количеством событий разобрались, теперь стоит взглянуть, сколько пользователей совершали каждое из этих событий за исследуемый промежуток времени.

In [20]:
user_logs = (dataset.pivot_table(index='event_name',
                                 values='user_id',
                                 aggfunc='nunique')
        .sort_values(by='user_id',ascending=False))
#Посмотрим, сколько процентов пользователей хотябы раз совершали действие
user_logs['at_least_1_time_%'] = round((user_logs['user_id']/
                                        dataset['user_id'].nunique())*100,2)
user_logs = user_logs.reset_index()
display(user_logs)
plt.figure(figsize=(15,8))
(sns.barplot(data = user_logs, orient='h', y='event_name', x='user_id', color='Green')
 .set(xlabel='Количество пользователей', ylabel='Событие'))
plt.title('Сколько пользователей совершали события'+'\n',
          color='SteelBlue', fontsize=20)
plt.show()
event_name user_id at_least_1_time_%
0 tips_show 2801 65.25
1 search 1666 38.81
2 map 1456 33.92
3 photos_show 1095 25.51
4 contacts_show 981 22.85
5 advert_open 751 17.49
6 favorites_add 351 8.18
7 tips_click 322 7.50
8 contacts_call 213 4.96
  • Больше всего пользователей смотрело на рекомендованные объявления - 2801 -около половины от всех пользователей.
  • На втором месте - событие search - его совершило 1666 пользователей.
  • На третьем месте - map- 1456 пользователей.
  • Меньше всего пользователей совершили событие contacts_call - 213 пользователей
  • Целевое действие совершили только 981 пользователь. Хотябы раз это действие выполнили 22.85% людей.

Сессии¶

К содержанию

Нас интересует, как же пользователи ведут себя в приложении. Для этого нам необходимо посмотреть когда и какие действия они совершают. Попробуем определить, как долго пользователи впринципе проводят в приложении.

In [21]:
dataset = dataset.sort_values(['user_id','event_time']).reset_index(drop=True)
dataset['time_dif'] = (dataset.groupby('user_id')['event_time']
                       .diff(1).fillna(value=dt.timedelta(seconds = 0)))
dataset['time_dif'].quantile([.75,.90,.91,.92,.93,.94,.95,.99])
Out[21]:
0.75      0 days 00:02:47.395096
0.90   0 days 00:14:01.701345800
0.91   0 days 00:20:02.879849240
0.92   0 days 00:33:46.308574200
0.93   0 days 01:05:43.862410839
0.94   0 days 02:32:08.823209039
0.95   0 days 07:06:28.640042799
0.99   4 days 15:04:05.563057072
Name: time_dif, dtype: timedelta64[ns]

Мы рассмотрели, какие перерывы между совершениями событий делают пользователи, чтобы определить максимальное время сессии - времени, которое пользователь провел в приложении без выхода из профиля или перехода приложения в "спящий режим".

  • Около 6% пользователей могут сделать следующее действие через 7 часов, это уже слишком много и скорее всего является следующей сессией.
  • Большинство пользователей следующее действие совершает в течение 30-40 минут.

Попробуем взять за отсечку бездействия 3 часа.

  • Пользователей, которые дольше сидят - всего 6%
  • Это реально возможное время, которое пользователь может провести в приложении,
  • его хватит на то, чтобы посмотреть несколько вариантов.
  • Да и я считаю, что трудно сразу найти что-то стоящее и навряд ли многие пользователи будут покупать что-то не разобравшись.
In [22]:
gap = (dataset.groupby('user_id')['event_time'].diff() > pd.Timedelta('3Hour')).cumsum()
dataset['session_id'] = dataset.groupby(['user_id', gap], sort=False).ngroup() + 1
dataset.head()
Out[22]:
event_time event_name user_id source time_dif session_id
0 2019-10-07 13:39:45.989359 tips_show 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 0 days 00:00:00 1
1 2019-10-07 13:40:31.052909 tips_show 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 0 days 00:00:45.063550 1
2 2019-10-07 13:41:05.722489 tips_show 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 0 days 00:00:34.669580 1
3 2019-10-07 13:43:20.735461 tips_show 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 0 days 00:02:15.012972 1
4 2019-10-07 13:45:30.917502 tips_show 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 0 days 00:02:10.182041 1

Итак, мы разделили все события пользователей на сессии, посмотрим, какое количество сессий приходится в среднем на пользователей и сколько всего сессий.

In [23]:
print('Общее количество сессий:', dataset['session_id'].nunique())
sessions_per_user = dataset.pivot_table(index='user_id',
                                        values='session_id',
                                        aggfunc='nunique')

fig, axes = plt.subplots(1, 2, figsize=(15, 5), sharey=False)
fig.suptitle('Количество сессий на пользователя', color='SteelBlue', fontsize=20)

sns.boxplot(ax=axes[0],
            data = sessions_per_user,
            y='session_id', 
            notch=True, 
            showcaps=False,
            flierprops={'marker': 'x'},
            boxprops={'facecolor': 'cadetblue'},
            medianprops={'color': 'r', 'linewidth': 2}).set(ylabel='Количество сессий')
sns.boxplot(ax=axes[1],
            data = sessions_per_user,
            y='session_id', 
            notch=True, 
            showcaps=False,
            flierprops={'marker': 'x'},
            boxprops={'facecolor': 'cadetblue'},
            medianprops={'color': 'r', 'linewidth': 2}).set(ylabel='Количество сессий')
axes[1].set_ylim(0,5)

plt.show()
Общее количество сессий: 8613

Пользователи совершили 8613 сессий. В среднем пользователи совершали 1-2 сессии. Однако, есть пользователи, которые успели совершить около 40 сессий за исследуемое время.

Популярные сценарии совершения событий¶

К содержанию

Теперь приступим к определению сценариев событий, которые совершали пользователи во время своих сессий. Для этого сгруппируем наш датафрейм по уникальным пользователям и уникальным сессиям, которые совершали пользователи.

In [24]:
#Получаем датафрейм в разрезе по пользователям и сессиям, с датами начала и конца сессий, 
#чтобы получить длительность
#А также количеством совершенных действий и списком уникальных событий, 
#совершенных во время сессии, и источник пользователя
sessions = dataset.groupby(['user_id','session_id']).agg({'event_time':['first', 'last'],
                                                          'event_name':['unique','count'],
                                                           'source':'first'}).reset_index()
#Переименуем столбцы, из-за мультииндексации
sessions.columns = ['user_id',
                    'session_id',
                    'session_start',
                    'session_end',
                    'path',
                    'event_count',
                    'source']
#получим длительность сессий в секундах (чтобы проще было оценивать различия в длительности)
sessions['duration'] = (sessions['session_end'] - sessions['session_start']).dt.total_seconds()
#Получим колонку с буллевыми значениями, чтобы разделить сессии на те, 
#в которых есть целевое действие и те, в которых нет
#Для этого напишем функцию
def check_events(event_list):
    ''' Функция проверяет, есть ли в списке событий целевое событие contacts_show
    и возвращает буллево значение '''
    if 'contacts_show' in event_list:
        return True
    else:
        return False
#Применим функцию для создания колонки
sessions['target_action'] = sessions['path'].apply(check_events)
sessions
Out[24]:
user_id session_id session_start session_end path event_count source duration target_action
0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 1 2019-10-07 13:39:45.989359 2019-10-07 13:49:41.716617 [tips_show] 9 other 595.727258 False
1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2 2019-10-09 18:33:55.577963 2019-10-09 18:42:22.963948 [map, tips_show] 4 other 507.385985 False
2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 3 2019-10-21 19:52:30.778932 2019-10-21 20:07:30.051028 [tips_show, map] 14 other 899.272096 False
3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 4 2019-10-22 11:18:14.635436 2019-10-22 11:30:52.807203 [map, tips_show] 8 other 758.171767 False
4 00157779-810c-4498-9e05-a1e9e3cedf93 5 2019-10-19 21:34:33.849769 2019-10-19 21:59:54.637098 [search, photos_show] 9 yandex 1520.787329 False
... ... ... ... ... ... ... ... ... ...
8608 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 8609 2019-10-30 11:31:45.886946 2019-10-30 11:31:45.886946 [tips_show] 1 google 0.000000 False
8609 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 8610 2019-11-01 00:24:31.162871 2019-11-01 00:24:53.473219 [tips_show] 2 google 22.310348 False
8610 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 8611 2019-11-02 01:16:48.947231 2019-11-02 01:16:48.947231 [tips_show] 1 google 0.000000 False
8611 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 8612 2019-11-02 18:01:27.094834 2019-11-02 19:30:50.471310 [tips_show, contacts_show] 6 google 5363.376476 True
8612 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 8613 2019-11-03 14:32:55.956301 2019-11-03 16:08:25.388712 [tips_show, contacts_show] 29 google 5729.432411 True

8613 rows × 9 columns

Теперь определим, какие последовательности событий чаще всего встречались в нашем датасете и исключим те сесси, которые начинались с целевого события. Такое возможно, но нас интересует, что пользователи делают прежде, чем совершают просмотр контактов продавца. Да и нам неизвестно как устроен интерфейс приложения и нет другой информации, которая помогла бы лучше узнать поведение пользователей.

In [25]:
#Получим только те сессии, где в списке событий есть целевое действие
target_sessions = sessions.query('target_action == True').reset_index()
#Выведем все возможные списки событий в лист, чтобы посчитать повторяющиеся
events = target_sessions['path'].tolist()

#Так как списки событий хранятся в виде массива, с ними сложно работать, обратимся к функции,
#чтобы избавиться от массива
def convert_name(string):
    ''' Функция проверяет, есть ли в строке искомое значение (название события)
    и возвращает его аббревиатуру  '''
    if string == 'advert_open':
        return 'ao'
    if string == 'photos_show':
        return 'ps'
    if string == 'tips_show':
        return 'ts'
    if string == 'tips_click':
        return 'tp'
    if string == 'contacts_show':
        return 'cs'
    if string == 'contacts_call':
        return 'cc'
    if string == 'map':
        return 'mp'
    if string == 'search':
        return 'sc'
    if string == 'favorites_add':
        return 'fa'

#создадим пустой список, который будет получать список аббревиатур не в виде массива
event_list = []
#Для этого пройдёмся по всем массивам(последовательностям) в листе events
for array in events:
    #И получим все элементы этих массивов и разделим на строки
    event_elements = []
    event_string = ''
    #Все названия из массива сделаем аббревиатурами с помощью нашей функции 
    #и разделим их нижним подчеркиванием.
    #Получим строку с аббревиатурами и передадим в пустой лист, который создали ранее
    for element in array:
        event_elements.append(convert_name(element))
    event_string = '_'.join(event_elements)
    event_list.append(event_string)
#event_list

#Добавим переработанный список массивов в качестве столбца в наш датасет целевых сессий
target_sessions['path_abb'] = pd.Series(event_list)
#Наконец получим датафрейм с популярными последовательностями событий
event_counts = target_sessions['path_abb'].value_counts().to_frame().reset_index()
event_counts = event_counts.rename(columns= {'index' : 'event_names'})
#print(event_counts)

#Получим только те последовательности, где целевое событие не является первым 
event_counts = event_counts[event_counts['event_names'].str.contains('\w(cs)')]
event_counts.head(15)
Out[25]:
event_names path_abb
0 ts_cs 262
2 mp_ts_cs 81
4 ps_cs 75
5 sc_cs_cc 51
7 sc_ps_cs 46
8 sc_cs 45
9 ps_cs_cc 38
11 ts_cs_mp 31
12 sc_ts_cs 31
14 ts_mp_cs 25
15 ts_cs_tp 24
16 mp_cs_ts 19
17 sc_ps_cs_cc 17
18 ts_tp_cs 15
19 sc_cs_ts 13
  • Чаще всего пользователи переходят на целевое действие из просмотра рекомендаций товаров - 262 сессии содержат такой сценарий.
  • Также, популярным является сценарий, где пользователи из карты сайта переходят на просмотр рекомендаций, и после этого опять же на целевое действие.
  • Среди наиболее распространенных сценариев почти не встречаются те, где более трех действий.
  • Пользователи могут продолжать совершать действия после просмотра контактов.

Теперь, чтобы построить воронки, нам необходимо отметить, какие уникальные действия совершали пользователи, чтобы строить воронку только по тем пользователям, которые точно совершали все события из популярных последовательностей - чтобы не получить веретено вместо воронки.

In [26]:
#Усовершенствуем наш датасет, чтобы было проще отбирать людей, которые совершали все действия
#из популярных сценариев для построения воронок
def get_event_string(lst):
    ''' Создает строку с уникальными действиями пользователей в виде сокращений, если пользователь
    хотябы раз совершал данное действие'''
    list = []
    for event in lst:
        list.append(convert_name(event))
    return '_'.join(list)

# Для каждого пользователя получим строку с уникальными совершенными событиями
user_event_string = dataset.groupby('user_id')['event_name'].unique().apply(get_event_string)

# Объединяем результат с исходным датафреймом
dataset['user_events'] = dataset['user_id'].map(user_event_string)

dataset
Out[26]:
event_time event_name user_id source time_dif session_id user_events
0 2019-10-07 13:39:45.989359 tips_show 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 0 days 00:00:00 1 ts_mp
1 2019-10-07 13:40:31.052909 tips_show 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 0 days 00:00:45.063550 1 ts_mp
2 2019-10-07 13:41:05.722489 tips_show 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 0 days 00:00:34.669580 1 ts_mp
3 2019-10-07 13:43:20.735461 tips_show 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 0 days 00:02:15.012972 1 ts_mp
4 2019-10-07 13:45:30.917502 tips_show 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 other 0 days 00:02:10.182041 1 ts_mp
... ... ... ... ... ... ... ...
74192 2019-11-03 15:51:23.959572 tips_show fffb9e79-b927-4dbb-9b48-7fd09b23a62b google 0 days 00:00:27.886483 8613 ts_mp_cs
74193 2019-11-03 15:51:57.899997 contacts_show fffb9e79-b927-4dbb-9b48-7fd09b23a62b google 0 days 00:00:33.940425 8613 ts_mp_cs
74194 2019-11-03 16:07:40.932077 tips_show fffb9e79-b927-4dbb-9b48-7fd09b23a62b google 0 days 00:15:43.032080 8613 ts_mp_cs
74195 2019-11-03 16:08:18.202734 tips_show fffb9e79-b927-4dbb-9b48-7fd09b23a62b google 0 days 00:00:37.270657 8613 ts_mp_cs
74196 2019-11-03 16:08:25.388712 tips_show fffb9e79-b927-4dbb-9b48-7fd09b23a62b google 0 days 00:00:07.185978 8613 ts_mp_cs

74197 rows × 7 columns

Мы получили датасет, где для каждого пользователя есть список уникальных действий, которые пользователь совершал хотябы раз. Данный список нам еще пригодится в дальнейшем.

Воронки¶

К содержанию

Теперь мы можем построить воронки событий по нашим популярным сценариям. Отберем 6 первых популярных сценариев, так как частота их встречаемости близка к 50 или выше.

In [28]:
#Зададим параметр топ-последовательностей событий
chart_num = 6

#А теперь обратно получим список событий, чтобы построить воронки
#Для этого создадим функцию, которая из аббревиатур получает полные названия событий
def unroll_code(string):
    ''' Функция проверяет налчие в строке определенной аббревиатуры
    и возвращает полное название события '''
    if string == 'ao':
        return 'advert_open'
    if string == 'ps':
        return 'photos_show'
    if string == 'ts':
        return 'tips_show'
    if string == 'tp':
        return 'tips_click'
    if string == 'cs':
        return 'contacts_show'
    if string == 'cc':
        return 'contacts_call'
    if string == 'mp':
        return 'map'
    if string == 'sc':
        return 'search'
    if string == 'fa':
        return 'favorites_add'

#Напишем функцию, которая получает список последовательностей
def unroll_list(string):
    ''' Функция создаёт массив(список) последовательностей
    из строки с аббревиатурами и создаёт лист списков '''
    string_list = []
    string = string.split('_')
    for element in string:
        string_list.append(unroll_code(element))
    return string_list

#Создадим пустой список, который будет получать списки из датафрейма с популярными событиями
funnel_data = []
#Создаем лист с топом последовательностей событий
popular_funnel = event_counts['event_names'].head(chart_num).tolist()
#Разворачиваем последовательности из аббревиатур и добавляем их в пустой список
for funnel in popular_funnel:
    funnel_data.append(unroll_list(funnel))
    
#Зададим маски, чтобы считать количество пользователей на каждом этапе с учетом предыдущего     
mask_ts = dataset['user_events'].str.contains('ts') 
mask_mp = dataset['user_events'].str.contains('mp')
mask_ps = dataset['user_events'].str.contains('ps') 
mask_cs = dataset['user_events'].str.contains('cs')
mask_sc = dataset['user_events'].str.contains('sc')
mask_cc = dataset['user_events'].str.contains('cc')
mask_ts = dataset['user_events'].str.contains('ts')
mask_tp = dataset['user_events'].str.contains('tp')
mask_fa = dataset['user_events'].str.contains('fa')

#Функция, чтобы получать маску
def decode_mask(string):
    return f'@mask_{string}'

#Функция, чтобы создавать маску для последующих этапов
def build_mask(string):
    ev_str = string.split('_')
    result = ''
    for i in range(len(ev_str)):
        if i == 0:
            result = decode_mask(ev_str[i])
        else:
            result = result + ' & ' + decode_mask(ev_str[i])
    return result

#Получаем число уникальных пользователей для каждого этапа
#С учётом участия в предыдущих этапах
funnel_x_all = []
for event_chain in popular_funnel:
    events = event_chain.split('_')
    mask_list = []
    en_list = []
    for event in events:
        mask_list.append(event) 
        string = '_'.join(mask_list)
        res_string = build_mask(string)
        event_number = dataset.query(f'{res_string}')['user_id'].nunique()
        en_list.append(event_number)
    funnel_x_all.append(en_list)
    
#Построим воронки событий на одном графике
#Для этого определим количество субграфиков на графике и получим количество строк и столбцов
if len(funnel_data) % 2 == 0:
    row_number = 2
    col_number = int(len(funnel_data) / 2)
else:
    row_number = 2
    col_number = int((len(funnel_data) // 2) + 1)

#Строим фигуру с субграфиками, которая получает рассчитываемые координаты
fig = make_subplots(rows=row_number, cols=col_number, horizontal_spacing=0.11)
#И первое значение списка популярных последовательностей для воронки
k = 0
#Которая будет в зависимости от значения из списка последовательностей
#будет получать последовательность для воронки
#И строить график в соответствующем субграфике
#И продолжать так для всех последующих воронок
for i in range(1,row_number+1):
    for j in range(1,col_number+1):
        if k != len(funnel_data):
            fig.add_trace(go.Funnel(
                y = funnel_data[k],
                x = funnel_x_all[k],
                textposition = "inside",
                textinfo = "value+percent previous" ),
                row=i, col=j)
            k = k + 1
        else:
            pass
        
#Зададим параметры для всего графика
fig.update_layout(
    autosize=True,
    showlegend = False,
    title='Воронки событий по популярным паттернам поведения пользователей',
    yaxis_title='События',
    font=dict(
        family="arial",
        size=12,
        color="darkslateblue"
    ))

fig.show()

Самая большая конверсия в целевое действие отмечается в следующих паттернах:

  • Просмотр фото товара - просмотр контактов - 26,7%
  • Просмотр "карты" объявлений - просмотр рекомендаций - просмотр контактов - 20,3%
  • Поиск по сайту - просмотр фотографий - просмотр контактов - 13%

Просто показ рекомендаций приводит к совершению целевого действия только в 10% случаев. Также, стоит отметить, что не всегда пользователям удается что-то найти.

Длительности сессий¶

К содержанию

Теперь посмотрим, сколько вообще сессий можно считать успешными.

In [30]:
plt.figure(figsize=(8,8))
sessions['target_action'].value_counts().plot.pie(autopct='%.0f%%',
                                                  ylabel='',
                                                  fontsize=14,
                                                  labels=None)
plt.legend(labels=['Не совершалось целевое действие','Совершалось целевое действие'],
           loc='upper center')
plt.title('Соотношение сессий по типу', color='SteelBlue', fontsize=20)
plt.show()

Только в 17% сессий пользователи совершили целевое действие. Похоже, что не так много пользователей могут найти что-то интересующее их.

  • Возможно это связано с огромным количеством постоянно появляющихся позиций ("наличие выбора - причина невроза"),
  • Или недостаточным количеством информации о вещах.
  • Может дело в плохо настроенных рекомендациях для пользователя, если они вообще настраиваются.
  • Ну и поведение человека может быть связано с внешними, неподвластными нам факторами.

Теперь оценим, есть ли различия в длительности между успешными и неуспешными сессиями.

In [31]:
fig, axes = plt.subplots(1, 2, figsize=(15, 5), sharey=False);
fig.suptitle('Соотношение длительности сессии с видом сессии', color='SteelBlue', fontsize=20);
sns.boxplot(ax=axes[0],
            data=sessions,
            x='target_action',
            y='duration').set(xlabel='Совершение целевого действия', 
                              ylabel='Продолжительность сессии в секундах')
axes[0].set_xticklabels(['Нет','Да'])
sns.boxplot(ax=axes[1],
            data=sessions,
            x='target_action',
            y='duration').set(xlabel='Совершение целевого действия', 
                              ylabel='Продолжительность сессии в секундах')
axes[1].set_ylim(0,11000)
axes[1].set_xticklabels(['Нет','Да'])

plt.show();

Сессии, которые приводят к совершению целевого действия в среднем длятся дольше, чем сессии, где не совершается целевое действие. Медиана длительности таких сессий составляет около получаса. В среднем, успешные сессии не длятся более 1 часа. Однако, в обоих случаях встречаются сессии, которые длятся запредельно долго. Этому может быть несколько причин:

  • Мы неправильно установили отсечку бездействия пользователя
  • Это те пользователи, которые совершают очень много действий
  • Кто-то очень сильно хочет найти вещь своей мечты
  • А может в данных встречаются технические/администраторские логи.

Относительная частота событий¶

К содержанию

Посмотрим, какие еще есть различия между пользователями, которые совершали целевое действие, и не совершали целевое действие. А именно - проверим, отличаются ли доли совершаемых событий в двух группах пользователей.

In [32]:
ta = dataset['user_events'].str.contains('cs')
no_ta = ~dataset['user_events'].str.contains('cs')

target_group = dataset.query('@ta')['event_name'].value_counts().to_frame().sort_values(by='event_name')
target_group['share'] = round((target_group['event_name']/target_group['event_name'].sum())*100)
no_target_group = dataset.query('@no_ta')['event_name'].value_counts().to_frame().sort_values(by='event_name')
no_target_group['share'] = round((no_target_group['event_name']/no_target_group['event_name'].sum())*100)


fig, axes = plt.subplots(1, 2, figsize=(15, 5))

ax1 = target_group.plot(kind='barh', y='share',legend=False, ax=axes[0], xlabel='Событие')
ax1.set_title('Среди пользователей, совершавших целевое действие')
ax1.set_xlabel('Доля от всех событий,%')
for i in ax1.containers:
    ax1.bar_label(i,fmt='%1.f%%',label_type='edge')

ax2 = no_target_group.plot(kind='barh', y='share',legend=False, ax=axes[1])
ax2.set_title('Среди пользователей, не совершавших целевое действие')
ax2.set_xlabel('Доля от всех событий,%')
for i in ax2.containers:
    ax2.bar_label(i,fmt='%1.f%%',label_type='edge')

fig.suptitle('Относительная частота событий'+'\n', fontsize=20, color='SteelBlue')
plt.show()

Судя по графикам, в обеих группах все события соотносятся друг с другом одинаково:

  • Наибольшая доля у события tips_show,
  • Наименьшая доля у события tips_click,
  • Не меняется "топ" популярных событий.

Единственное отличие - пользователи, которые не совершают целевое действие - просмотр контактов, не совершают событие contacts_call. Логично предположить, что это связано с интерфейсом приложения - нельзя позвонить по номеру, который не посмотрел.

Анализ источников¶

К содержанию

Проанализируем наши источники, из которых пришли пользователи.

In [33]:
plt.figure(figsize=(15,8))
sns.barplot(data=(dataset
                  .pivot_table(index='source',values='user_id',aggfunc='nunique')
                  .sort_values(by='user_id',ascending=False)
                  .reset_index()), x='source', y='user_id').set(xlabel='Источник', 
                                                                ylabel='Количество пользователей')
plt.title('Распределение пользователей по источнику привлечения'+'\n',
          color='SteelBlue',fontsize=20)
plt.show()

Больше всего людей пришло из Яндекс'а, меньше всего людей пришло из Google. Было бы интересно также взглянуть на состав источников в группе others, но этой информации у нас нет. Однако, из нескольких источников людей пришло больше, чем из Google.

Теперь посмотрим, пользователи из какого источника охотнее совершали целевое действие.

In [34]:
plt.figure(figsize=(15,8))
sns.barplot(data=(dataset
                  .query('event_name == "contacts_show"')
                  .pivot_table(index='source',values='user_id',aggfunc='nunique')
                  .sort_values(by='user_id',ascending=False).reset_index()),
            x='source',
            y='user_id').set(xlabel='Источник',ylabel='Количество пользователей')
plt.title('Количество пользователей, совершивших целевое событие в разрезе по источникам'+'\n',
          color='SteelBlue', fontsize=20)
plt.show()

В совершении пользователями целевого действия также лидирует Яндекс. На этот раз Google занимает второе место. Меньше всего целевое действие совершали пользователи из других источников, не смотря на то, что от них пользователей пришло больше, чем из Google. Однако, без расчета конверсии делать выводы о успешности источников пока рано. Конверсию рассмотрим далее.

Проверка гипотез¶

К содержанию

Одни пользователи совершают действия tips_show и tips_click , другие — только tips_show . Проверьте гипотезу: конверсия в просмотры контактов различается у этих двух групп:¶

К содержанию

У нас есть две группы пользователей: те, кто только просматривают рекомендуемые объявления, и те, кто просматривают объявления и переходят по ним. Нас интересует, насколько эта разница в поведении влияет на конверсию и можно ли это как-то использовать для увеличения конверсии в приложении в целом.

Для этого проведем проверку следующей гипотезы:

  • Нулевая гипотеза - среднее значение конверсии групп не отличаются,
  • Альтернативная гипотеза - среднее значение конверсии групп отличается.

Проверять гипотезу мы будем сравнением долей.

In [35]:
def tips_groups(string):
    ''' Функция проверяет, есть ли в списке событий совершенных пользователем искомое значение
    и возвращает название группы '''
    if ('ts' in string) and not ('tp' in string):
        return 'only_show'
    elif ('ts' in string) and ('tp' in string):
        return 'show_click'
    else:
        return False
#Применим функцию для создания колонки
dataset['tips'] = dataset['user_events'].apply(tips_groups)
#dataset

#Теперь получим события для групп
groups = dataset.pivot_table(index='event_name',
                             values='user_id',
                             columns='tips', 
                             aggfunc='nunique')
groups = groups.T
groups = groups.merge(dataset.pivot_table(index='tips',
                                          values='user_id',
                                          aggfunc='nunique'),on='tips')
groups = groups[['contacts_show','user_id']].drop([False],axis=0).reset_index()
groups['conversion'] = round(groups['contacts_show']/groups['user_id']*100)
display(groups)
plt.figure(figsize=(15,8))
sns.barplot(data=groups, x='tips', y='conversion').set(xlabel='Совершенные действия',
                                                       ylabel='Конверсия, %')
plt.title('Значение конверсии в зависимости от действий с рекомендациями'+'\n',
          color='SteelBlue', fontsize=20)
plt.xticks(ticks=[0,1],labels=['Только просмотр','Просмотр и переход'])
plt.show()
tips contacts_show user_id conversion
0 only_show 425.0 2504 17.0
1 show_click 91.0 297 31.0

Разница между конверсией пользователей в этих двух группах различается почти в два раза. Теперь стоит оценить, насколько эта разница критична.

In [36]:
alpha = 0.05

trial_1 = groups['user_id'][0]
succes_1 =groups['contacts_show'][0]

trial_2 =groups['user_id'][1]
succes_2 =groups['contacts_show'][1]

p1 = succes_1/trial_1
p2 = succes_2/trial_2

p_combined = (succes_1 + succes_2) / (trial_1 + trial_2)

difference = p1 - p2 

z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trial_1+ 1/trial_2))

distr = st.norm(0, 1)  

p_value = (1 - distr.cdf(abs(z_value))) * 2

print('p-значение: ', p_value)

if p_value < alpha:
    print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
    print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными') 
p-значение:  9.218316554537864e-09
Отвергаем нулевую гипотезу: между долями есть значимая разница

Разница конверсий имеет статистическую значимость, что не удивительно, ведь она достаточно велика. Похоже, что наиболее охотно совершают целевое действие пользователи, которые переходят на рекламное объявление. Это добавляет аргументов в пользу теории, что есть какая-то проблема в рекомендациях для пользователей.

Различается ли конверсия в просмотры контактов у группы пользователей пришедших из Яндекса и у группы пользователей пришедших из Google:¶

К содержанию

Мы определили, что из источника Яндекс пришло больше всего пользователей, и больше всего пользователей совершали целевое действие. Но теперь стоит оценить конверсию у двух ведущих источников и сравнить, отличается ли она и имеет ли это статистическую значимость.

Для этого проведем проверку гипотез:

  • Нулевая гипотеза - среднее значение конверсии группы из Яндекса не отличается от среднего значения конверсии группы из Google
  • Альтернативная гипотеза - средние значения конверсии двух групп различаются.

Для проверки воспользуемся методом сравнения долей, так как выборки отличаются. Тест будет двусторонним, так как нам нужно только проверить есть ли отличия и насколько они значимы.

In [37]:
source_groups = dataset.pivot_table(index='event_name',
                                    values='user_id',
                                    columns='source', 
                                    aggfunc='nunique')
source_groups = source_groups.T
source_groups = source_groups.merge(dataset.pivot_table(index='source',
                                                        values='user_id',
                                                        aggfunc='nunique'),on='source')
source_groups = source_groups[['contacts_show','user_id']].drop(['other'],axis=0).reset_index()
source_groups['conversion'] = round(source_groups['contacts_show']/source_groups['user_id']*100)
display(source_groups)
plt.figure(figsize=(15,8))
sns.barplot(data=source_groups, x='source', y='conversion').set(xlabel='Источник',
                                                                ylabel='Конверсия, %')
plt.title('Значение конверсии в зависимости от источника'+'\n',
          color='SteelBlue', fontsize=20)
plt.show()
source contacts_show user_id conversion
0 google 275 1129 24.0
1 yandex 478 1934 25.0

Различие между конверсией составляет 1%, теперь оценим, насколько значима эта разница.

In [38]:
alpha = 0.05

trials1 = source_groups['user_id'][0]
hits1 = source_groups['contacts_show'][0]

trials2 = source_groups['user_id'][1]
hits2 =source_groups['contacts_show'][1]

p1 = hits1/trials1
p2 = hits2/trials2

p_combined = (hits1 + hits2) / (trials1 + trials2)

difference = p1 - p2 

z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials1 + 1/trials2))

distr = st.norm(0, 1)  

p_value = (1 - distr.cdf(abs(z_value))) * 2

print('p-значение: ', p_value)

if p_value < alpha:
    print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
    print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными') 
p-значение:  0.8244316027993777
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Что же, при всех отличиях между источниками, из которых пришли пользователи, их конверсия пользователей не отличается.

Выводы¶

К содержанию

В ходе работы мы провели анализ пользовательских логов приложения "Ненужные вещи" за 4 недели - с 7 октября по 3 ноября 2019 года. Вот, какие выводы можно сделать из данных:

  • За это время приложением воспользовалось 4293 пользователя,
  • Они совершили 74197 событий,
  • Всего пользователи совершали 9 уникальных видов событий,
  • В среднем каждый пользователь совершал около 17 действий,
  • Пользователи приходили из трех источников - Яндекс, Google, группы других источников,
  • Средняя продолжительность одной сессии пользователя составляет около часа,
  • Только 17% сессий являются успешными - то есть в них совершается целевое действие - просмотр контактов.

Какие закономерности можно отметить:

  • Успешные сессии в среднем длятся дольше - сессии без целевого действия редко длятся более 5 минут.
  • Доли событий, которые совершаются пользователями, хотябы раз совершившими целевое действие, не отличаются от долей событий, которые совершали пользователи, не совершившие целевое действие.
  • Пользователи, которые совершают не только просмотр рекомендованных объявлений, но и переход в объявление, имеют более высокий процент конверсии (чаще совершают целевое действие). Возможно это связано с некорректной настройкой рекомендаций.
  • Пользователи пришедшие из источников Яндекс и Google не имеют значимой разницы в конверсии в целевое действие.

Какие особенности поведения прослеживаются у пользователей:

  1. Мало пользователей заинтересованы в рекомендуемых объявлениях - объявления видят все пользователи и часто, но не так много людей переходит по этим объявлениям и совершают целевое действие.
  2. Наиболее вероятно пользователи совершают целевое действие, если они самостоятельно проводят поиск интересующих их вещей - вплоть до 23% от всех пользователей, которые совершили поиск
  3. Также, высока конверсия пользователей, которые просматривали фотографии объявлений - до 30%. Скорее всего это более информативные объявления, благодаря чему пользователям проще сделать выбор, или найти конкретную интересующую вещь.

Чтобы повысить заинтересованность пользователя, стоит рассмотреть следующие варианты улучшения:

  1. Добавить опрос для уточнения предпочтений пользователей на этапе регистрации/первой сессии;
  2. Строить рекомендации по используемым пользователем расширенным настройкам поиска, истории поиска. Или выводить их только после совершения поискового запроса;
  3. Добавить возможность убирать не интересующие предложения из рекомендаций
  4. Обязательным пунктом публикации объявления сделать наличие фотографий.